/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <baja/assert.hpp>
#include <journal/common/utils.hpp>
#include <journal/app/constants.h>
#include <journal/views/photo.h>
#include <journal/views/interfaces.h>
#include <journal/models/interfaces.h>
#include <journal/models/photo_model.h>
#include <journal/views/quad_renderer.h>
#include <baja/trace.hpp>
#include <journal/app/keys.h>
#include <journal/common/util.h>

namespace journal { namespace view {

namespace c = concurrency;

b::uint32 photo::_typeToken = 0;

photo::photo(
    const std::shared_ptr<model::iphoto_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    ) :
    _photoModel(model),
    _inputCapture(inputCapture), 
    _rebuildCache(true),
    _state(state::uninitialized),
    _photoObserverCookie(-1),
    _bringToFront(false),
    _alpha(1.f)
{
    static b::uint32 typeToken = constants::memoryTrackingService()->registerType(L"photo view", true);
    if (_typeToken == 0)
    {
        _typeToken = typeToken;
    }
    constants::memoryTrackingService()->logCreate(_typeToken, 0, sizeof(photo));
}

std::shared_ptr<photo> photo::create(
    const std::shared_ptr<model::iphoto_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<photo> returnValue = std::make_shared<photo>(model, inputCapture);
    returnValue->init();
    return returnValue;
}

photo::~photo()
{
    this->cleanup();
    constants::memoryTrackingService()->logDispose(_typeToken, 0, sizeof(photo));
}

void photo::init()
{
    BAJA_VERIFY(_state == state::uninitialized);
 
    std::shared_ptr<b::imemory_stream> ms = _photoModel->textureFileStream();

    _photoTexture = 
        constants::graphics()->createTextureFromFileInMemory(
            (b::memory*)ms->getBuffer(),
            static_cast<uint32>(ms->getBufferSize()),
        0, // usage
        g::pixel_format::fromSource,
        g::memory_pool::default,
        g::mipmap_load::forceMipmaps
        );

    try
    {
        // add ourselves as an observer of the photo model
        _photoObserverCookie = _photoModel->addObserver(
            std::dynamic_pointer_cast<model::iphoto_model_observer>(shared_from_this()));

        update(
            _photoModel->scale(), 
            _photoModel->position(), 
            _photoModel->angle()
                        );

        _state = state::idle;
        setRebuildCache(true);
    }
    catch (...)
    {
        constants::uiService()->showDialog(L"This application doesn't have permissions to open that file.  Try moving it to your pictures folder.");
    }
}

bool photo::renderToCache()
{
    bool returnValue = false;

    if (_state == state::uninitialized) return returnValue;

    if (this->shouldRebuildCache())
    {
        if (_state == state::idle || _state == state::underneathSelectedPhoto) // if moving, don't render the picture to the cache
        {
            this->renderPhoto(1.0f);
            returnValue = true;
        }
        _rebuildCache = false;
    }

    return returnValue;
}

bool photo::getDynamicBounds(m::rectangle* bounds)
{
    if (_state == state::moving)
    {
        // TRACK: 447: returning a full page bound but we can be more precise
        *bounds = common::fullPageBounds();

        return true;
    }

    return false;
}

void photo::renderDynamic()
{
    if (_state == state::moving) 
    {
        this->renderPhoto(0.65f * _alpha);
    }
    else if (_state == state::onTopOfSelectedPhoto)
    {
        this->renderPhoto(0.95f * _alpha);
    }
}

void photo::onPhotoMoved(
    const std::shared_ptr<model::iphoto_model>& photo,
    const b::float32& scale,
    const m::point2d& position,
    const b::float32& angle
    )
{
    ACQUIRE_VIEW_LOCK;
    BAJA_VERIFY(_state != state::uninitialized); // shouldn't really happen if we aren't initialized
    this->update(scale, position, angle);
    this->setRebuildCache(true);
}

void photo::onPointer(const a::pointer_args& args, void* context)
{
}

void photo::onGesture(const a::gesture_args& args, void* context)
{
    if (_state == state::uninitialized) return;

    page_pointer_context* ppc = static_cast<page_pointer_context*>(context);
    ppc->handled = false;

    if (args.deviceType != a::device_type::touch)
        return;

    if (_state == state::moving && args.type == a::gesture_event_type::update)
    {
        if (args.cumulative.scale != 1.0 || args.cumulative.rotation != 0)
        {
            _newScale = _holdScale * args.cumulative.scale;
            _newAngle = _holdAngle - args.cumulative.rotation;
        }
        else
        {
            _newPosition = _holdPosition;
            _newPosition.x += (ppc->localPoint.x - _holdPoint.x);
            _newPosition.y += (ppc->localPoint.y - _holdPoint.y);
        }

        this->update(
            _newScale,
            _newPosition,
            _newAngle
            );

        DEBUG_CONSOLE_TRACE(boost::wformat(L"moving photo: %0.2f -- (%0.2f, %0.2f) -- %0.2f") % _newScale % _newPosition.x % _newPosition.y % _newAngle);

        ppc->handled = true;
    }
    else if (_state == state::moving && args.type == a::gesture_event_type::complete)
    {
        _holdScale = _newScale;
        _holdPosition = _newPosition;
        _holdAngle = _newAngle;
        ppc->handled = true;
    }
    else if (_state == state::moving && args.type == a::gesture_event_type::start)
    {
        // grab the new hold point if the user releases and presses again
        _holdPoint = ppc->localPoint;
        ppc->handled = true;
    }
    else if (args.type == a::gesture_event_type::tap)
    {
        if (_state == state::moving)
        {
            this->applyChange();

            ppc->handled = true;
        }
    }
    else if (args.type == a::gesture_event_type::hold)
    {
        if (_state != state::moving)
        {
            m::point3d photoPoint;
            if (this->hitTest(ppc->localPoint, &photoPoint))
            {
                _inputCapture.lock()->setCapture(std::static_pointer_cast<a::iinput_observer>(shared_from_this()));
                _state = state::moving;
                _holdPoint = ppc->localPoint;
                _oldScale = _newScale = _photoModel->scale();
                _oldPosition = _newPosition = _photoModel->position();
                _oldAngle = _newAngle = _photoModel->angle();
                _holdScale = _newScale;
                _holdPosition = _newPosition;
                _holdAngle = _newAngle;

                constants::uiService()->showAppBar(a::app_bar_id::photo);
                
                // disable intertia for photo moves
                constants::shellService()->disableInertia();

                this->setRebuildCache(true);

                ppc->handled = true;
                this->raiseDirty();

                // raise photo selected event
                _observerSource.raise(
                    [&](const std::shared_ptr<view::iphoto_observer>& observer, void* context)
                    {
                        observer->onPhotoSelected(shared_from_this());
                    });
            }
        }
    }
}

void photo::endSelection()
{
    _inputCapture.lock()->releaseCapture();
    _state = state::idle;
    constants::shellService()->enableInertia(constants::general::inertialDeceleration());

    constants::uiService()->hideAppBar();

    // raise photo deselected event
    _observerSource.raise(
        [&](const std::shared_ptr<view::iphoto_observer>& observer, void* context)
        {
            observer->onPhotoDeselected();
        });
}

void photo::cancelChange()
{
    _bringToFront = false;
    this->endSelection();

    this->update(
        _oldScale,
        _oldPosition,
        _oldAngle
        );
    this->setRebuildCache(true);
}

void photo::applyChange()
{
    if (_bringToFront) 
    {
        _photoModel->parent()->bringPhotoToFront(_photoModel->storageId());
        _bringToFront = false;
    }
    this->endSelection();

    // raise photo moved event
    _observerSource.raise(
        [&](const std::shared_ptr<view::iphoto_observer>& observer, void* context)
        {
            observer->onPhotoMoved(
                shared_from_this(),
                _newScale,
                _newPosition,
                _newAngle
                );
        });
}

void photo::onKey(const a::key_args& args, void* context)
{
    if (_state == state::uninitialized) return;

    BAJA_VERIFY(context);
    bool* handled = static_cast<bool*>(context);
    *handled = false;
}

void photo::onCommand(const view::command_id id, void* context)
{
    if (id == view::command_id::align_photo)
    {
        BAJA_VERIFY(_state == state::moving);

        int angle = static_cast<int>(abs(_newAngle)) % 360;

        if (angle < 45) angle = 0;
        else if (angle <= 45 + 90 ) angle = 90; 
        else if (angle <= 45 + 180) angle = 180;
        else if (angle <= 45 + 270) angle = 270;
        else angle = 360;

        if (_newAngle < 0) angle *= -1;

        _newAngle = static_cast<b::float32>(angle);

        this->update(
            _newScale,
            _newPosition,
            _newAngle
            );
    }
    else if (id == view::command_id::bring_to_front_photo)
    {
        _bringToFront = true;
    }
    else if (id == view::command_id::ok_photo)
    {
        this->applyChange();
    }
    else if (id == view::command_id::cancel_photo)
    {
        this->cancelChange();
    }
}

void photo::onSiblingPhotoSelected(const b::Boolean onTopOfSelectedPhoto)
{
    _state = onTopOfSelectedPhoto ? state::onTopOfSelectedPhoto : state::underneathSelectedPhoto;
}

void photo::onSiblingPhotoDeselected()
{
    _state = state::idle;
}

void photo::beforeRemove()
{
    endSelection();
}

void photo::cleanup()
{
    if (_photoModel) _photoModel->removeObserver(_photoObserverCookie);
    _photoObserverCookie = -1;
}

m::point2d photo::calculatePhotoSize(const b::float32 scale)
{
    m::point2d size;

    size.x = (_photoTexture->getWidth() * scale);
    size.y = (_photoTexture->getHeight() * scale);

    return size;
}

bool photo::hitTest(const m::point3d& point, m::point3d* photoPoint)
{
    // calculate the photo's coordinate system matrix
    m::matrix44 t;

    t.setRotationXYZ(0, 0, m::degToRad(_photoModel->angle()));
    t.setTranslation(m::point3d(
        _photoModel->position().x,
        _photoModel->position().y,
        0.f
        ));

    m::matrix44 coordSystem;
    t.getInverse(&coordSystem);

    coordSystem.apply(point, photoPoint);

    m::point2d size = this->calculatePhotoSize(_photoModel->scale());

    if ((abs(photoPoint->x) < (size.x/2)) &&
        (abs(photoPoint->y) < (size.y/2)))
    {
        return true;
    }

    return false;
}

void photo::update(
    const b::float32 scale,
    const m::point2d& position,
    const b::float32 angle
    )
{
    _angle = m::degToRad(angle);
    _position = position;
    _size = this->calculatePhotoSize(scale);

    this->raiseDirty();
}

void photo::renderPhoto(const b::float32 alpha)
{
    std::shared_ptr<g::iengine> graphics = constants::graphics();

    constants::graphics()->setZBuffer(g::enabled_state::disabled);
    constants::graphics()->setCulling(g::cull_state::none);

    std::shared_ptr<quad_renderer> quad = quad_renderer::singleton();

    quad->render(
        alpha,
        _angle,
        constants::general::pageWorldDimensions(),
        _size,
        _position,
        _photoTexture,
        post_processing_effect::none,
        false // writeAlpha
        );
}

std::shared_ptr<iphoto> createPhoto(
    const std::shared_ptr<model::iphoto_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<iphoto> returnValue = photo::create(model, inputCapture);
    return std::static_pointer_cast<iphoto>(returnValue);
}

}} // namespace journal::view
